/**
 * AGNCone.java
 */
package agngui.agnmodel;
import java.util.ArrayList;
import java.util.List;

import javax.media.j3d.Appearance;
import javax.media.j3d.Geometry;
import javax.media.j3d.Material;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TriangleArray;
import javax.vecmath.AxisAngle4d;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import agngui.event.AGNEvent;
import agngui.event.HideCounterconeEvent;
import agngui.event.ShowCounterconeEvent;

/**
 * Manages the geometry of the cone scattering region.
 */
public class AGNCone extends AGNObject {

	/**
	 * the number of points on the top and bottom disk (there will be 2*n points total used in rendering)
	 */
    private int    n;
    
    /**
     * the vertices of the Counter cone
     */
    private Point3d[] counter_verts;
    
    /**
     * the normal vectors of the Counter cone
     */
    private Vector3f[] counter_norms;
    
    /**
     * the triangles representing the geometry of the Counter cone
     */
    private TriangleArray counter_triangles;
    
    /**
     * a flag that says whether or not the Counter cone is showing
     */
    private boolean cc_showing;

    /**
     * AGNCone
     *
     * Creates the cone geometry.
     *
     * @param Ri radius of the inner sphere 
     * @param Ro radius of the outer sphere
     * @param ha half-angle of the cone in degrees
     * @param n  Number of faces for the cone
     */
    public AGNCone( double Ri, double Ro, double ha, int n, boolean show_cc, AGNModelGeometry modelGeometry ) {
        super( AGNModelGeometry.SCATTERCONE, modelGeometry );

        this.Ri = Ri;
        this.Ro = Ro;
        this.ha = ha;
		this.n = n;

        geometry = createGeometry();
		for(Geometry g : geometry) {
			addGeometry( g );
		}

		cc_showing = show_cc;

        setAppearance( createAppearance() );

        transformGroup.addChild( this );
    }
	
    /**
     * showCounterCone
     *
     * Renders the counter cone visible or invisible.
     *
     * @param show if true, the counter cone is made visible; if false, the 
     *             counter cone is invisible.
     */
	public void showCountercone(boolean show) {
		if(show && !cc_showing) {
			addGeometry(counter_triangles);
			cc_showing = true;
		} else if(!show && cc_showing) {
			removeGeometry(counter_triangles);
			cc_showing = false;
		}
	}
	
	/**
	 * processes events in the system that are specific to the cone.
	 * Should only have to worry about the Show and Hide Countercone events
	 */
	public void processEvent(AGNEvent event) {
		super.processEvent(event);
		if(event instanceof ShowCounterconeEvent) {
			showCountercone(true);
		} else if(event instanceof HideCounterconeEvent) {
			showCountercone(false);
		}
	}

    /**
     * generatePoints
     * 
     * @param ri The radius of the circle on the "floor" of the cone
     * @param ro The radius of the circle on the "cap" of the cone
     * @param ha Half angle of the cone
     * @param n  Number of faces for the cone.
     * @return an array of points defining the cone in no useful order
     */
    private Point3d[] generatePoints( double ri, double ro, double ha, int n ) {
        double dTheta = (2 * Math.PI) / n;
        double xi, yi, zi; // Inner coordinates
        double xo, yo, zo; // Outer coordinates

        List<Point3d> verts = new ArrayList<Point3d>();
        List<Point3d> cc_verts = new ArrayList<Point3d>();
        List<Vector3f> normals = new ArrayList<Vector3f>();
        List<Vector3f> cc_norms = new ArrayList<Vector3f>();
        // Create initial line segment
        Point3d pi; // po pipo pipo po pi po
        Point3d po;
        Point3d topCenter, ccTopCenter;
        Point3d bottomCenter, ccBottomCenter;

        xi = ri;
		yi = 0.0;
        zi = ri / Math.tan( ha );
        pi = new Point3d( xi, yi, zi );

        xo = ro;
		yo = 0.0;
        zo = ro / Math.tan( ha );
        po = new Point3d( xo, yo, zo );

        topCenter    = new Point3d( 0.0, 0.0, zo );
        bottomCenter = new Point3d( 0.0, 0.0, zi );
        ccTopCenter = new Point3d(topCenter);
        ccTopCenter.negate();
        ccBottomCenter = new Point3d(bottomCenter);
        ccBottomCenter.negate();
        
        Point3d cc_pi = new Point3d(pi);
        cc_pi.setZ(-cc_pi.getZ());
        Point3d cc_po = new Point3d(po);
        cc_po.setZ(-cc_po.getZ());
        cc_verts.add(ccBottomCenter);
        cc_verts.add(ccTopCenter);
        cc_verts.add(cc_pi);
        cc_verts.add(cc_po);

        cc_norms.add(new Vector3f(0, 0, 1));//bottom center normal
        cc_norms.add(new Vector3f(0, 0, -1));//top center normal
        cc_norms.add(getVertexNormal(cc_pi, cc_po, ccBottomCenter));
        cc_norms.add(getVertexNormal(cc_po, cc_pi, ccTopCenter));

        normals.add(new Vector3f(0, 0, -1));//bottom center normal
        normals.add(new Vector3f(0, 0, 1));//top center normal
        normals.add(getVertexNormal(pi, po, bottomCenter));
        normals.add(getVertexNormal(po, pi, topCenter));
        verts.add( bottomCenter );
        verts.add( topCenter );
        verts.add( pi );
        verts.add( po );

        // Generate the points!
        for( double theta = dTheta; theta < (2 * Math.PI); theta += dTheta ) {
            Point3d po0, po1;
            Point3d pi0, pi1;

            pi0 = verts.get( 2 );
            po0 = verts.get( 3 );

			double cos_theta = Math.cos(theta);
			double sin_theta = Math.sin(theta);
			
            pi1 = new Point3d( (pi0.x * cos_theta - pi0.y * sin_theta),
                               (pi0.y * cos_theta + pi0.x * sin_theta),
                                pi0.z);
            po1 = new Point3d( (po0.x * cos_theta - po0.y * sin_theta),
                               (po0.y * cos_theta + po0.x * sin_theta),
                                po0.z);


            normals.add(getVertexNormal(pi1, po1, bottomCenter));
            normals.add(getVertexNormal(po1, pi1, topCenter));
            
            verts.add( pi1 );
            verts.add( po1 );
            
            Point3d cc_pi1 = new Point3d(pi1);
            Point3d cc_po1 = new Point3d(po1);
            cc_pi1.setZ(-cc_pi1.getZ());
            cc_po1.setZ(-cc_po1.getZ());
            cc_norms.add(getVertexNormal(cc_pi1, cc_po1, ccBottomCenter));
            cc_norms.add(getVertexNormal(cc_po1, cc_pi1, ccTopCenter));
            cc_verts.add(cc_pi1);
            cc_verts.add(cc_po1);
        }

        counter_norms = cc_norms.toArray(new Vector3f[0]);
        counter_verts = cc_verts.toArray(new Point3d[0]);

        norm_array = normals.toArray(new Vector3f[0]);
        return verts.toArray( new Point3d[0] );
    }
    
    /**
     * createGeometry
     *  
     * Creates the geometry to render the cone (shell, top disk, bottom disk)
     *
     * @return an array of geometry: index 0 = shell (1 triangle strip) 
     *                               index 1 = disks (2 triangle fans)
     */
    protected Geometry[] createGeometry() { 
        double ha = this.ha * Math.PI / 180;
        double ri = Ri * Math.tan( ha );
        double ro = Ro * Math.tan( ha );

        Point3d points[] = generatePoints( ri, ro, ha, n );

        List<Point3d> vertices = new ArrayList<Point3d>();
        List<Vector3f> normals = new ArrayList<Vector3f>();
        
        List<Point3d> cc_vertices = new ArrayList<Point3d>();
        List<Vector3f> cc_normals = new ArrayList<Vector3f>();
        Geometry geometryArray[];

        // Generate top disk
        for( int i = 3, counter = 0; counter < n; counter++, i += 2 ) {
			int second_point = i;
			int third_point = (i + 2) % (2*n+2);
			if(third_point < 3) {
				third_point += 2;
			}
			
			vertices.add( points[1] );
			vertices.add( points[second_point] );
			vertices.add( points[third_point] );
			normals.add(norm_array[1]);
			normals.add(norm_array[second_point]);
			normals.add(norm_array[third_point]);
			
			cc_vertices.add( counter_verts[1] );
			cc_vertices.add( counter_verts[third_point] );
			cc_vertices.add( counter_verts[second_point] );
			cc_normals.add(counter_norms[1]);
			cc_normals.add(counter_norms[third_point]);
			cc_normals.add(counter_norms[second_point]);
			
        }

        // Generate bottom disk
        int cap = 2 * n + 1;
        for( int i = cap-1, counter = 0; counter < n; counter++, i -= 2 ) {
        	vertices.add( points[0] );
        	vertices.add( points[i] );
			int third_point = i - 2;
			if(third_point < 2) {
				third_point = cap-1;
			}
			vertices.add( points[third_point] );
			
			normals.add(norm_array[0]);
			normals.add(norm_array[i]);
			normals.add(norm_array[third_point]);
			
			//switch the normals around because they're opposites
			cc_vertices.add( counter_verts[0] );
			cc_vertices.add( counter_verts[third_point] );
			cc_vertices.add( counter_verts[i] );
			cc_normals.add(counter_norms[0]);
			cc_normals.add(counter_norms[third_point]);
			cc_normals.add(counter_norms[i]);
			
        }

        /* 2, 4, 3
         * 3, 4, 5
         * 4, 6, 5
         * 5, 6, 7
         */
        boolean in_order = false;
        // Generate shell
        for( int i = 2; i < points.length; i++ ) {
			int index2 = i+2, index3 = i+1;
			
			if(i == points.length - 2) {
				index2 = 2;
				in_order = false;
			} else if(i == points.length - 1) {
				index2 = 3;
				index3 = 2;
				in_order = true;
			}
			vertices.add( points[i] );
			normals.add(norm_array[i]);
			cc_vertices.add( counter_verts[i] );
			cc_normals.add(counter_norms[i]);
			
            if(in_order) {
            	vertices.add( points[index3] );
            	vertices.add( points[index2] );

    			normals.add(norm_array[index3]);
    			normals.add(norm_array[index2]);

    			cc_vertices.add( counter_verts[index2] );
    			cc_vertices.add( counter_verts[index3] );
    			cc_normals.add(counter_norms[index2]);
    			cc_normals.add(counter_norms[index3]);
    			
            } else {
            	vertices.add( points[index2] );
            	vertices.add( points[index3] );
    			normals.add(norm_array[index2]);
    			normals.add(norm_array[index3]);
    			
    			cc_vertices.add( counter_verts[index3] );
    			cc_vertices.add( counter_verts[index2] );
    			cc_normals.add(counter_norms[index3]);
    			cc_normals.add(counter_norms[index2]);
            }
            in_order = !in_order;
        }
        
        norm_array = normals.toArray(new Vector3f[0]);
        point_array = vertices.toArray(new Point3d[0]);
        TriangleArray triangles = new TriangleArray( vertices.size(), TriangleArray.COORDINATES | TriangleArray.NORMALS );
        triangles.setCoordinates(0, vertices.toArray( new Point3d[0] ) );
        triangles.setNormals(0, norm_array);
        
        counter_triangles = new TriangleArray(cc_vertices.size(), TriangleArray.COORDINATES | TriangleArray.NORMALS);
        counter_triangles.setCoordinates(0, cc_vertices.toArray(new Point3d[0]));
        counter_triangles.setNormals(0, cc_normals.toArray(new Vector3f[0]));
	
        counter_verts = cc_vertices.toArray(new Point3d[0]);
        counter_norms = cc_normals.toArray(new Vector3f[0]);
        geometryArray = new Geometry[]{ triangles };

		generateHandles();
        
        if(cc_showing) {
      	    addGeometry(counter_triangles);
        }

        return geometryArray;
    }

    /**
     * createAppearance
     *
     * Creates the initial appearance for the cone:
     * .5f transparency
     * (0, 0, 1) diffuse color
     * (0, 0, .4) ambient color
     *
     * @return The Appearance reference for the cone.
     */
    private Appearance createAppearance() {
		setTransparency(0.5f);
        Appearance retVal = getAppearance();
        Material mat = retVal.getMaterial();
        mat.setDiffuseColor(new Color3f(0, 0, 1.0f));
        mat.setAmbientColor(new Color3f(0, 0, 0.4f));
        retVal.setMaterial(mat);
        return retVal;
    }

    /**
     * generateHandles
     *
     * Regenerates all of the cone's handles according to the cone parameters.
     */
    public void generateHandles() {
        Point3d     points[]                = new Point3d[5];
        Vector3d    linHandleAxes           = new Vector3d( 0.0, 0.0, 1.0 );
        Vector3d    anHandleAxes            = new Vector3d( 0.0, 1.0, 0.0  );
        Vector3d    rotHandleAxisPolar      = new Vector3d( -1.0, 0.0, 0.0  );
        Vector3d    rotHandleAxisAzimuthal  = new Vector3d( 0.0, 0.0, 1.0 );
        AxisAngle4d polarRotation           = new AxisAngle4d();
        Transform3D axisTransform           = new Transform3D();
        Transform3D objectTransform         = new Transform3D();
        double ha = this.ha * Math.PI / 180;
        double xA, zA;
        double rotRadius = Ro / 4.0;
        double recentPolarValue     = (handles == null) ? 0.0 : handles[3].getRecentValue();
        double recentAzimuthalValue = (handles == null) ? 0.0 : handles[4].getRecentValue();

        if( handles == null ) {
            handles = new AGNParameterHandle[points.length];
        } else {
            // Transform polar axis according to azimuthal angle
            polarRotation.set( rotHandleAxisAzimuthal, recentAzimuthalValue );
            axisTransform.setRotation( polarRotation );
        }
        objectTransform = getFullTransform();

        // Inner radius handle
        points[0] = new Point3d( 0.0, 0.0, Ri );
        
        // Outer radius handle
        points[1] = new Point3d( 0.0, 0.0, Ro );

        // Angular handle
        xA = Ro * Math.tan( ha );
        zA = Ro;
        points[2] = new Point3d( xA, 0.0, zA );

        // Polar handle
        points[3] = new Point3d( Ro, 0.0, rotRadius );
        points[3].scale( SCALE_FACTOR );

        // Azimuthal handle
        points[4] = new Point3d( rotRadius, 0.0, Ro );
        points[4].scale( SCALE_FACTOR );

        objectTransform.transform( points[0] );
        objectTransform.transform( points[1] );
        objectTransform.transform( points[2] );
        axisTransform.transform( points[3] );
        objectTransform.transform( linHandleAxes );
        objectTransform.transform( anHandleAxes  );
        axisTransform.transform( rotHandleAxisPolar );

        //POLAR_AXIS_OF_ROTATION = new Vector3d( rotHandleAxisPolar );

		handles[0] = new AGNLinearParameterHandle(  this, points[0], linHandleAxes, ID_RI );
		handles[1] = new AGNLinearParameterHandle(  this, points[1], linHandleAxes, ID_RO );
		handles[2] = new AGNAngularParameterHandle( this, points[2], anHandleAxes, ID_HA );
        handles[3] = new AGNRotationalParameterHandle( this, points[3], rotHandleAxisPolar, 
                                                             recentPolarValue, ID_ROT_POLAR );
        handles[4] = new AGNRotationalParameterHandle( this, points[4], rotHandleAxisAzimuthal,
                                                             recentAzimuthalValue, ID_ROT_AZIMUTHAL );
    }

}
